或许在这之前你想使用并行计算:R语言并行化基础与提高
在大猫师兄的强烈推荐下,我研究了利用data.table包做分组计算。
以往做分组计算的思路是这样子的:先按照某个变量(比如股票stock)将数据集划分(split)为一个列表,内含若干个小数据框,然后自定义一个可以处理每个小数据框的函数。最后就可以利用lapply函数对列表中每个数据框做遍历了,这样子的速度很慢,可以用parallel包做个加速。为此我还写了个模板,记录下来,以后备用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| FUNC=out temp=temp library(parallel) library(rlist) system.time({ cl=makeCluster(detectCores()) clusterExport(cl, 'FUNC', envir = .GlobalEnv) clusterEvalQ(cl,{ library(fPortfolio) }) re=parLapplyLB(cl,temp,FUNC) stopCluster(cl) }) result=list.rbind(re)
|
然而,开4核跑并行,做李翔的滚动回归时,要3分钟才可以出来结果,根据大猫师兄的说法,用data.table,都是1秒的事。在我算所有上市公司的市盈率和账面市值比BM等指标的时候,因为要把月频的财报折算为日频数据,并不是1秒钟可以解决的事情。当然可以继续优化下去,但data.table做分组计算确实是目前最快的选择。
主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| fillTdays=function(data,tdays,stock.include=T) { data=as.data.frame(data) if(class(data$StockIssuing_date)!='Date') data$StockIssuing_date=as.Date(as.character(data$StockIssuing_date)) out=data.frame(matrix(NaN,ncol = ncol(data)-1,nrow = length(tdays))) if(stock.include) { names(out)=names(data)[-3]; out$stock=data$stock[1] } else { names(out)=names(data)[-2] } if(class(tdays)!='Date') out$mydate=as.Date(tdays) targetPoint=which(!is.na(data$StockIssuing_date)) for(p in targetPoint) { value=data[p,(3+stock.include):ncol(data)] out[out$mydate>=data$StockIssuing_date[p],(2+stock.include):ncol(out)]=value } return(out) } stament=as.data.table(stament) setkey(stament,stock,mydate) test2=stament[,(fillTdays(.SD,tdays,stock.include = F)),by='stock']
|
从上面最后一句可以看出来,stament
是data.table数据类型,列的位置上(fillTdays(.SD,tdays,stock.include = F))
是最关键的用法,fillTdays是自定义的函数,返回的是一个数据框,.SD是stament里除去stock列的剩余数据(因为by=stock作为了分组变量),你可以将.SD理解为某一stock对应的数据框,不含stock列。
重要提示来了,在写函数时,要注意输入参数data是从.SD传递过来的,因此它也data.table格式,这个时候我们要转换成data.frame格式才可以在函数内部随心所欲地做自己喜欢做的事,返回值的类型也应该是data.frame类型,但这个并不影响最终的结果类型(依然是data.table)。
另一个重要提示是,如果函数返回的是一列数而非一个多列的数据框,那么应该这样写:
1
| test2=stament[,.(mydate,fillTdays(.SD,tdays,stock.include = F)),by='stock']
|
这是因为.( )
表示数据框列表,.(mydate,fillTdays(.SD,tdays,stock.include = F))
恰好就是一个数据框列表,内含两列,而上面的stament[,(fillTdays(.SD,tdays,stock.include = F)),by='stock']
没有使用.( )
是因为函数的返回值就已经是数据框的类型了。
目前data.table还有一个超快的用法就是排序,这跟用R原生的order比较时就可以感受出来了。
1
| setkey(stament,stock,mydate)
|
这条语句,不仅把我的财务数据先按stock排序,再按日期mydate排序,最重要的是设置了主键,这在merge里面充分震撼了我。1900万的两个数据merge只需要几秒。果然data.table很值得研究各种有意思的玩法。